package ar.com.miguelgriffa.secureproperties;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Properties;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class SecureProperties extends java.util.Properties {
	private static transient final java.util.logging.Logger log = java.util.logging.Logger.getLogger(SecureProperties.class.getName());

	private static final String ALGO = "DES";

	// key size: DES 56, 192/256 AES
	private Key key = new SecretKeySpec(new byte[] {
			0, 1, 2, 3, 4, 5, 6, 7
	}, ALGO);

	private final Cipher cipher;

	private final Cipher decipher;

	/**
	 * Helper method that uses the context classloader of the current thread to load the given resource.
	 * if the resource is not found nothing happends.
	 * if an IOException occurs, it is encapsulated in a RuntimeException
	 * 
	 * @param resource name of the resource to load
	 * @throws RuntimeException if an IOException occurs in load
	 * @throws IllegalArgumentException if resource is <code>null</code>
	 */
	public void load(final String resource) {
		if (resource == null) {
			throw new IllegalArgumentException("resource name cannot be null");
		}
		final InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
		if (is == null) {
			log.severe("Resource " + resource + " not found in classpath, refusing to load()");
		} else {
			try {
				load(is);
			} catch (final IOException e) {
				throw new RuntimeException(e);
			}
		}
	}

	@Override
	public synchronized void load(final InputStream inStream) throws IOException {
		super.load(inStream);
		// decrypt each value
		for (final Iterator it = keySet().iterator(); it.hasNext();) {
			final String key = (String) it.next();
			final String p = (String) get(key);
			log.fine("loading property(" + key + "): " + p);
			if (p != null) {
				try {
					final String plainVal = new String(decrypt(p));
					log.fine("plainVal: " + plainVal);
					put(key, plainVal);
				} catch (final IllegalBlockSizeException ex) {
					throw new IOException("Error setting property " + key + ": " + ex);
				} catch (final BadPaddingException ex) {
					throw new IOException("Error setting property " + key + ": " + ex);
				}
			} else {
				log.severe("property value for " + p + " was null");
			}
		}
	}

	@Override
	public synchronized void store(final OutputStream out, final String comments) throws IOException {
		for (final Iterator it = keySet().iterator(); it.hasNext();) {
			final String key = (String) it.next();
			final String p = (String) get(key);
			log.fine("storing property(" + key + "): " + p);
			if (p != null) {
				try {
					put(key, encryptAsText((p.getBytes())));
				} catch (final IllegalBlockSizeException ex) {
					throw new IOException("" + ex);
				} catch (final BadPaddingException ex) {
					throw new IOException("" + ex);
				}
			} else {
				log.severe("property is null: " + key);
			}
		}
		super.store(out, comments);
	}

	/**
	 * Create a new secure propoerties.
	 * 
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchPaddingException
	 * @throws InvalidKeyException
	 */
	public SecureProperties() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException {
		cipher = Cipher.getInstance(ALGO);
		cipher.init(Cipher.ENCRYPT_MODE, key);
		decipher = Cipher.getInstance(ALGO);
		decipher.init(Cipher.DECRYPT_MODE, key);
	}

	/**
	 * Create a new secure propoerties adding all values from the given plain
	 * properties object.
	 * 
	 * @param plain
	 * @throws InvalidKeyException
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchPaddingException
	 */
	public SecureProperties(final Properties plain) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
		this();
		this.putAll(plain);
	}

	public String encryptAsText(final byte[] data) throws IllegalBlockSizeException, BadPaddingException {
		final byte[] bs = cipher.doFinal(data);
		return encode(bs);
	}

	public byte[] decrypt(final String data) throws IllegalBlockSizeException, BadPaddingException {
		final byte[] bs = decipher.doFinal(decode(data));
		return bs;
	}

	private String encode(final byte[] data) {
		return new String(Base64.encodeBase64(data));
	}

	private byte[] decode(final String data) {
		return Base64.decodeBase64(data.getBytes());
	}

	public byte[] decrypt(final byte[] data) throws IllegalBlockSizeException, BadPaddingException {
		final byte[] bs = decipher.doFinal(data);
		return bs;
	}

	/**
	 * Main para convertir un properties plano a uno encriptado
	 * 
	 * @param args
	 */
	public static void main(final String[] args) {
		// FIXME internationalize, do not evolve too much this, use an external class, using commons-cli for proper handling of options
		if (args == null || args.length != 2) {
			System.err.println("parametros incorrectos debe especificar solo dos argumentos");
			System.err.println("El primero es el archivo origen, el properties a encriptar");
			System.err.println("El segundo es el archivo destino, el properties encriptado a escribir");
		} else {
			try {
				final FileInputStream in = new FileInputStream(args[0]);
				final File o = new File(args[1]);
				if (o.exists()) {
					System.err.println("El archivo destino " + o + " ya existe, NO se sobreescribira");
					System.err.println("Especifique otro archivo o borrelo");
					System.exit(-1);
				}
				final FileOutputStream out = new FileOutputStream(o);

				final Properties p1 = new Properties();
				p1.load(in);

				final SecureProperties p2 = new SecureProperties();
				p2.putAll(p1);
				p2.store(out, "Generated");
				out.close();

			} catch (final Exception e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Set the key to be used for encryption/decryption.
	 * 
	 * @param key must be exactly 8 chars long
	 * @throws IllegalArgumentException if key is <code>null</code> or not 8 chars long
	 */
	public void setKey(final String key) {
		if (key == null || key.length() != 8) {
			throw new IllegalArgumentException("key must be exactly 8 chars long");
		}
		this.key = new SecretKeySpec(key.getBytes(), ALGO);
	}
}
